// Copyright 2022 Jeroen van Nugteren

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// include header
#include "dfarrayangular.hh"

// common headers
#include "rat/common/extra.hh"

// distmesh headers
#include "dftransform.hh"
#include "dfunion.hh"

// code specific to Rat
namespace rat{namespace dm{

	// constructor
	DFArrayAngular::DFArrayAngular(){
		set_name("Angular Array");
	}

	// constructor
	DFArrayAngular::DFArrayAngular(
		const ShDistFunPr& df, 
		const arma::uword num_sym, 
		const arma::uword num_array,
		const fltp radius,
		const fltp phi) : DFArrayAngular(){
		add_df(df); set_num_sym(num_sym); set_num_array(num_array); 
		set_radius(radius); set_phi(phi);
	}

	// constructor
	DFArrayAngular::DFArrayAngular(
		const std::list<ShDistFunPr>& dfs, 
		const arma::uword num_sym, 
		const arma::uword num_array,
		const fltp radius,
		const fltp phi) : DFArrayAngular(){
		for(const auto& df : dfs)add_df(df); 
		set_num_sym(num_sym); set_num_array(num_array); 
		set_radius(radius); set_phi(phi);
	}

	// factory
	ShDFArrayAngularPr DFArrayAngular::create(){
		return std::make_shared<DFArrayAngular>();
	}

	// factory
	ShDFArrayAngularPr DFArrayAngular::create(
		const ShDistFunPr& df, 
		const arma::uword num_sym, 
		const arma::uword num_array,
		const fltp radius,
		const fltp phi){
		return std::make_shared<DFArrayAngular>(df,num_sym,num_array,radius,phi);
	}

	// factory
	ShDFArrayAngularPr DFArrayAngular::create(
		const std::list<ShDistFunPr>& dfs, 
		const arma::uword num_sym, 
		const arma::uword num_array,
		const fltp radius,
		const fltp phi){
		return std::make_shared<DFArrayAngular>(dfs,num_sym,num_array,radius,phi);
	}

	// add parts
	void DFArrayAngular::add(const ShDistFunPr& df){
		add_df(df);
	}

	// add distance function
	arma::uword DFArrayAngular::add_df(const ShDistFunPr &df){
		const arma::uword index = df_.size()+1;
		df_.insert({index,df}); return index;
	}
	
	// retreive distance function at index
	const ShDistFunPr& DFArrayAngular::get_df(const arma::uword index) const{
		auto it = df_.find(index);
		if(it==df_.end())rat_throw_line("index does not exist");
		return (*it).second;
	}
	
	// delete distance function at index
	bool DFArrayAngular::delete_df(const arma::uword index){	
		auto it = df_.find(index);
		if(it==df_.end())return false;
		(*it).second = NULL; return true;
	}

	// number of distance functions
	arma::uword DFArrayAngular::num_df() const{
		return df_.size();
	}

	// re-index nodes after deleting
	void DFArrayAngular::reindex(){
		std::map<arma::uword, ShDistFunPr> new_df; arma::uword idx = 1;
		for(auto it=df_.begin();it!=df_.end();it++)
			if((*it).second!=NULL)new_df.insert({idx++, (*it).second});
		df_ = new_df;
	}

	// setters
	void DFArrayAngular::set_num_sym(const arma::uword num_sym){
		num_sym_ = num_sym;
	}

	void DFArrayAngular::set_radius(const fltp radius){
		radius_ = radius;
	}

	void DFArrayAngular::set_phi(const fltp phi){
		phi_ = phi;
	}

	void DFArrayAngular::set_num_array(const arma::uword num_array){
		num_array_ = num_array;
	}

	// getters
	arma::uword DFArrayAngular::get_num_sym()const{
		return num_sym_;
	}

	fltp DFArrayAngular::get_radius()const{
		return radius_;
	}

	fltp DFArrayAngular::get_phi()const{
		return phi_;
	}

	arma::uword DFArrayAngular::get_num_array()const{
		return num_array_;
	}

	arma::uword DFArrayAngular::get_num_base()const{
		return num_array_==0 ? num_sym_ : num_array_;
	}

	// create union
	ShDistFunPr DFArrayAngular::create_array()const{
		// check validity
		is_valid(true);

		// create union distance function
		const ShDFUnionPr dfuni = DFUnion::create();

		// number of base
		const arma::uword num_base = get_num_base();

		// walk over bases
		for(arma::uword i=0;i<num_base;i++)
			dfuni->add_df(DFTransform::create(DFUnion::create(df_),{radius_,0},i*arma::Datum<fltp>::tau/num_sym_ + phi_));

		// return distance function
		return dfuni;
	}


	// perimeter function
	ShPerimeterPr DFArrayAngular::create_perimeter(const fltp delem) const{
		return create_array()->create_perimeter(delem);
	}

	// get bounding box
	arma::Mat<fltp>::fixed<2,2> DFArrayAngular::get_bounding() const{
		return create_array()->get_bounding();
	}

	// get fixed points
	arma::Mat<fltp> DFArrayAngular::get_fixed(const fltp abstol) const{
		return create_array()->get_fixed(abstol);
	}

	// distance function
	arma::Col<fltp> DFArrayAngular::calc_distance(const arma::Mat<fltp> &p) const{
		return create_array()->calc_distance(p);
	}

	// validity check
	bool DFArrayAngular::is_valid(const bool enable_throws)const{
		if(df_.empty()){if(enable_throws){rat_throw_line("requires at least one input distance function");} return false;};
		for(auto it=df_.begin();it!=df_.end();it++)if(!(*it).second->is_valid(enable_throws))return false;
		if(num_sym_==0){if(enable_throws){rat_throw_line("number of symmetries must be at least one");} return false;};
		if(radius_<0){if(enable_throws){rat_throw_line("radius must be larger than zero");} return false;};
		return true;
	}

	// type string for serialization
	std::string DFArrayAngular::get_type(){
		return "rat::dm::dfangulararray";
	}

	// method for serialization into json
	void DFArrayAngular::serialize(Json::Value &js, cmn::SList &list) const{
		// serialize parent
		DistFun::serialize(js,list);

		// store type
		js["type"] = get_type();

		js["num_sym"] = static_cast<int>(num_sym_);
		js["num_array"] = static_cast<int>(num_array_);
		js["radius"] = radius_;
		js["phi"] = phi_;

		// distance functions
		for(auto it=df_.begin();it!=df_.end();it++)
		 	js["distfuns"].append(cmn::Node::serialize_node((*it).second,list));
	}

	// method for deserialisation from json
	void DFArrayAngular::deserialize(
		const Json::Value &js, cmn::DSList &list, 
		const cmn::NodeFactoryMap &factory_list, 
		const boost::filesystem::path &pth){
		
		// deserialize parent
		DistFun::deserialize(js,list,factory_list,pth);

		// parent
		set_num_sym(js["num_sym"].asUInt64());
		set_num_array(js["num_array"].asUInt64());
		set_radius(js["radius"].ASFLTP());
		set_phi(js["phi"].ASFLTP());

		// create distance functions
		for(auto it = js["distfuns"].begin();it!=js["distfuns"].end();it++){
			const ShDistFunPr df = cmn::Node::deserialize_node<DistFun>(*it, list, factory_list, pth);
			if(df==NULL)rat_throw_line("could not deserialize distance function");
			add_df(df);
		}
	}

}}